library(rlang)
library(purrr)
#>
#> Attaching package: 'purrr'
#> The following objects are masked from 'package:rlang':
#>
#> %@%, flatten, flatten_chr, flatten_dbl, flatten_int,
#> flatten_lgl, flatten_raw, invoke, splice
20 Evaluation
Introduction
“引用”有两个反面——“解引用”和“评估”。通常,“解引用”面向使用者,赋予了使用者选择性评估“表达式”的能力;而“评估”面向开发者,赋予了开发者在自定义环境中评估“表达式”的能力。
本章从最纯粹的评估模式开始讨论,介绍eval()
如何在环境中评估一个表达式,及如何使用它实现许多重要的base R 函数。然后延申评估,介绍两种重要思想:
Quoseure: 一种用于捕获表达式及其关联环境的数据结构。
数据掩码:使在“数据框环境”中评估表达式更加容易。
总之,准引用、quosure、数据掩码共同构成了我们所说的整洁评估(tidy-eval)。整洁评估为非标准评估提供了一种原则性的方法,使得可以交互使用这些函数并将其与其他函数进行嵌套。整洁评估是所有这些理论中最重要的实际含义,因此将花一些时间来探讨它们的含义。本章最后讨论了base R的最相关方法,以及如何围绕它们的缺点进行编程。
Outline
20.2节:介绍
base::eval()
函数,及如何使用它实现local()
和source()
。20.3节:介绍quosure数据结构,及如何生成与评估它。
20.4节:介绍数据掩码和避免歧义的声明。
20.5节:介绍使用整洁评估的实例。
20.6节:介绍base R中的非标准性评估及其缺陷。
Prerequisites
要求熟悉前两章内容和第7章有关环境的内容。
Evaluation basics
eval()
函数有两个参数:epxr
,env
。
expr
参数是待评估的“表达式”或符号。由于eval()
函数不会对输入引用,所以需要与expr()
一同使用:
<- 10
x eval(expr(x))
#> [1] 10
<- 2
y eval(expr(x + y))
#> [1] 12
env
参数用来指定评估“表达式”的“环境”,如果没有指定,则默认为当前环境。
eval(expr(x + y), env(x = 1000))
#> [1] 1002
当指定了环境,却没有引用输入时,会导致错误的结果:
eval(print(x + 1), env(x = 1000))
#> [1] 11
#> [1] 11
eval(expr(print(x + 1)), env(x = 1000))
#> [1] 1001
在了解了基础知识后,让我们探索一些应用,根据底层原理重新实现base R中的某些函数。
Application:local()
有时我们会创建一些临时变量来执行一系列计算,这些临时变量不会长期使用,可能也会相当占用内存,需要在使用结束后删除。一种方法是使用rm()
清楚临时变量;另一种是将计算过程打包为函数,仅调用一次。更优雅的方式是使用local()
函数,它可以创建一个临时环境,并执行其中的代码。
# Clean up variables created earlier
rm(x, y)
<- local({
foo <- 10
x <- 200
y + y
x
})
foo#> [1] 210
x#> Error: object 'x' not found
y#> Error: object 'y' not found
local()
函数的本质很简单,我们可以采用下面的策略实现它。首先捕获输入的“表达式”,然后使用local()
函数的执行环境作为eval()
的调用环境参与评估。
<- function(expr) {
local2 <- env(caller_env())
env eval(enexpr(expr), env)
}
<- local2({
foo <- 10
x <- 200
y + y
x
})
foo#> [1] 210
x#> Error: object 'x' not found
y#> Error: object 'y' not found
但base::local()
的底层实现很复杂,它使用了eval()
和substitute()
。
Application:source()
我们可以通过组合eval()
和parse_expr()
来实现source()
的功能。首先从磁盘中读取文件,然后使用parse_expr()
将字符串转换成“表达式”列表,最后使用eval()
评估“表达式”。实现如下:
<- function(path, env = caller_env()) {
source2 <- paste(readLines(path, warn = FALSE), collapse = "\n")
file <- parse_exprs(file)
exprs
<- NULL
res for (i in seq_along(exprs)) {
<- eval(exprs[[i]], env)
res
}
invisible(res)
}
真实的base::source()
函数更加复杂,会打印输入输出信息,同时有许多额外参数控制行为。
Expression vectors
上一章讲到,base::parse()
函数解析字符串时,如果捕获到多个“表达式”,会返回一个包含多个表达式的向量。base::eval()
函数可以直接评估这个向量,而不用上面的for
循环。
<- function(file, env = parent.frame()) {
source3 <- parse(file)
lines <- eval(lines, envir = env)
res invisible(res)
}
Gotcha:function()
如果你使用eval()
和expr()
来生成函数,有一个小小的漏洞需要注意:
<- 10
x <- 20
y <- eval(expr(function(x, y) !!x + !!y))
f
f#> function (x, y)
#> 10 + 20
这个函数看起来不像能正常运行,其实可以:
f()
#> [1] 30
这是因为,如果函数有“srcref”属性,就会打印它,但“srcref”是一个base R的特性,它无法识别准引用。
要解决这个问题,可以使用new_function()
或删除“srcref”属性:
attr(f, "srcref") <- NULL
f#> function (x, y)
#> 10 + 20
Quosures
几乎eval()
的所有使用都包括“表达式”和“环境”两个参数,但是base R中没有能同时提供这两个参数的数据结构,“rlang”包创建了这种数据结构——“quosures”。quosures是“quoting”和“closure”的复合体,意味着它同时包含了“表达式”和环境。
在本节中,你将学习如何创建和操作quosure, 以及一些关于如何实现它。
Creating
有三中方式创建quosure:
- 使用
enquo()
和enquos()
,它们会同时捕获表达式和环境。许多quosure都是由此创建的。
<- function(x) enquo(x)
foo foo(a + b)
#> <quosure>
#> expr: ^a + b
#> env: global
- 使用
quo()
和quos()
,与enquo()
和enquos()
的关系可以参考expr()``enexpr()
。使用的场景很少。
quo(x + y + z)
#> <quosure>
#> expr: ^x + y + z
#> env: global
- 使用
new_quosure()
,输入“表达式”和环境来创建quosure。使用场景也极少。
new_quosure(expr(x + y), env(x = 1, y = 10))
#> <quosure>
#> expr: ^x + y
#> env: 0x000001c76004dd30
Evaluting
只能使用eval_tidy()
来评估quosure。
<- new_quosure(expr(x + y), env(x = 1, y = 10))
q1 eval_tidy(q1)
#> [1] 11
Dots
enquos()
可以正确识别...
中传入的参数及其绑定的环境。例如,下面的qs
对象,正确评估了global
和f
的环境。
<- function(...) {
f <- 1
x g(..., f = x)
}<- function(...) {
g enquos(...)
}
<- 0
x <- f(global = x)
qs
qs#> <list_of<quosure>>
#>
#> $global
#> <quosure>
#> expr: ^x
#> env: global
#>
#> $f
#> <quosure>
#> expr: ^x
#> env: 0x000001c75ec5b668
map_dbl(qs, eval_tidy)
#> global f
#> 0 1
Under the hood
Quosures 数据结构受R中的“formulas”启发,因为“formula”同样也是同时捕获“表达式”与“环境”。早期也确实使用“formula”来进行评估,但因为无法简单的将~
变为准引用函数,所以放弃使用“formula”。
<- ~ runif(3)
f str(f)
#> Class 'formula' language ~runif(3)
#> ..- attr(*, ".Environment")=<environment: R_GlobalEnv>
Quosures 同样也是“formula”的子类:
<- new_quosure(expr(x + y + z))
q4 class(q4)
#> [1] "quosure" "formula"
这意味着一些函数可以直接作用于Quosures:
is_call(q4)
#> [1] TRUE
1]]
q4[[#> Warning: Subsetting quosures with `[[` is deprecated as of rlang 0.4.0
#> Please use `quo_get_expr()` instead.
#> This warning is displayed once every 8 hours.
#> `~`
2]]
q4[[#> x + y + z
有一个用于存放环境的属性:
attr(q4, ".Environment")
#> <environment: R_GlobalEnv>
但是不建议使用上面的函数,而是使用get_expr()
和get_env()
来获取表达式和环境:
get_expr(q4)
#> x + y + z
get_env(q4)
#> <environment: R_GlobalEnv>
Nested quosures
准引用支持在“表达式”中引入quosures,这是一种高级技术,使得创建嵌套quosures变得可能。例如下面的“表达式”中嵌套了两个短语。
<- new_quosure(expr(x), env(x = 1))
q2 <- new_quosure(expr(x), env(x = 10))
q3
<- expr(!!q2 + !!q3) x
它可以被正确地评估,但是如果打印它,你会发现它的“formula”形式:
eval_tidy(x)
#> [1] 11
x#> (~x) + ~x
可以使用rlang::expr_print()
来更好的展示,在终端中根据不同环境源显示不同颜色:
expr_print(x)
#> (^x) + (^x)
Data masks
本节介绍数据掩码(data mask)相关内容,这是一种同时在“环境”与“数据框构成的环境”中评估“表达式”的技术。它的核心思想与base R中的with()
,subset()
和transform()
类似,被广泛应用在“tidyverse”系列包中。
注意:enquo()
保证了能在不同环境中正确评估“表达式”中的变量,expr()
不能,这是一个重要的区别。但是本节所有示例中的enquo()
与expr()
都可以替换,不影响结果。
Basics
数据掩码允许你混合环境来源和数据框来源的变量。你可以将数据框当作环境变量传递给eval_tidy()
的第二个参数。
<- new_quosure(expr(x * y), env(x = 100))
q1 <- data.frame(y = 1:10)
df
eval_tidy(q1, df)
#> [1] 100 200 300 400 500 600 700 800 900 1000
上面的代码可能有些难以理解,我们可以作一些拆分:
<- 100
x <- data.frame(y = 1:10)
df eval_tidy(expr(x * y), df)
#> [1] 100 200 300 400 500 600 700 800 900 1000
稍加修改,改写为类似base::with()
的函数:
<- function(data, expr) {
with2 <- enquo(expr)
expr eval_tidy(expr, data)
}
with2(df, x * y)
#> [1] 100 200 300 400 500 600 700 800 900 1000
base::eval()
可以实现类似的效果,传递数据框到第二个参数,环境到第三个参数。
<- function(data, expr) {
with3 <- substitute(expr)
expr eval(expr, data, caller_env())
}
Pronouns
数据掩码会引起歧义。例如,在以下代码中,除非你知道df
中包含哪些变量,否则你无法知道x
是来自数据掩码还是环境。
with2(df, x)
#> [1] 100
为了解决歧义问题,数据掩码提供了两个声明:.data
和.env
。
.data$x
表示数据掩码中的变量x
。.env$x
表示环境中的变量x
。
<- 1
x <- data.frame(x = 2)
df
with2(df, .data$x)
#> [1] 2
with2(df, .env$x)
#> [1] 1
对于两个声明,你可以使用[[
,但是要注意它们是特殊的对象,和真实的数据框、环境不同。例如,如果找不到变量,它会抛出错误:
$y
df#> NULL
with2(df, .data$y)
#> Error in `.data$y`:
#> ! Column `y` not found in `.data`.
Application: subset()
下面是subset()
的使用场景之一:直接通过某个“表达式”进行过滤数据框的行。
<- data.frame(a = 1:5, b = 5:1, c = c(5, 3, 1, 4, 1))
sample_df
# Shorthand for sample_df[sample_df$a >= 4, ]
subset(sample_df, a >= 4)
#> a b c
#> 4 4 2 4
#> 5 5 1 1
# Shorthand for sample_df[sample_df$b == sample_df$c, ]
subset(sample_df, b == c)
#> a b c
#> 1 1 5 5
#> 5 5 1 1
subset()
的核心逻辑是:
- 两个参数:数据框
data
和“表达式”rows
。 - 在数据框
data
中,评估rows
,并返回结果逻辑向量。 - 根据逻辑向量,返回数据框的行。
<- function(data, rows) {
subset2 <- enquo(rows)
rows <- eval_tidy(rows, data)
rows_val stopifnot(is.logical(rows_val))
= FALSE]
data[rows_val, , drop
}
subset2(sample_df, a >= 4)
#> a b c
#> 4 4 2 4
#> 5 5 1 1
Application: transform()
transform()
函数类似dplyr::mutate()
,可以在数据框中添加新的一列。
<- data.frame(x = c(2, 3, 1), y = runif(3))
df transform(df, x = -x, y2 = 2 * y)
#> x y y2
#> 1 -2 0.9811686 1.9623372
#> 2 -3 0.4927797 0.9855595
#> 3 -1 0.2881747 0.5763493
下面是transform()
的简单等价实现:
<- function(.data, ...) {
transform2 <- enquos(...)
dots
for (i in seq_along(dots)) {
<- names(dots)[[i]]
name <- dots[[i]]
dot
<- eval_tidy(dot, .data)
.data[[name]]
}
.data
}
transform2(df, x2 = x * 2, y = -y)
#> x y x2
#> 1 2 -0.9811686 4
#> 2 3 -0.4927797 6
#> 3 1 -0.2881747 2
Application: select()
数据掩码不总是作用于数据框,也可以是list。这是subset()
的另一个使用场景——根据“表达式”选择某些列——的底层逻辑。
<- data.frame(a = 1, b = 2, c = 3, d = 4, e = 5)
df subset(df, select = b:d)
#> b c d
#> 1 2 3 4
它的关键思想是创建一个有name属性的list,list的每个元素是对应列的位置索引。
<- as.list(set_names(seq_along(df), names(df)))
vars str(vars)
#> List of 5
#> $ a: int 1
#> $ b: int 2
#> $ c: int 3
#> $ d: int 4
#> $ e: int 5
然后在list中进行评估,返回位置索引:
<- function(.data, ...) {
select2 <- enquos(...)
dots
<- as.list(set_names(seq_along(.data), names(.data)))
vars <- unlist(map(dots, eval_tidy, vars))
cols
= FALSE]
.data[, cols, drop
}select2(df, b:d)
#> b c d
#> 1 2 3 4
Using tidy evaluation
本节将会给出一些使用tidy evaluation的函数例子。
Quoting and unquoting
嵌套函数传递“表达式”时,函数内部一定要先使用enquo()
引用“表达式”,然后再使用!!
解引用。
假设有这样一个随机排序数据框的函数:
<- function(df, n) {
resample <- sample(nrow(df), n, replace = TRUE)
idx = FALSE]
df[idx, , drop }
现在要结合上面的subset2()
函数,同时实现筛选和随机排序:
<- function(df, cond, n = nrow(df)) {
subsample <- subset2(df, cond)
df resample(df, n)
}
<- data.frame(x = c(1, 1, 1, 2, 2), y = 1:5)
df rm(x)
subsample(df, x == 1)
#> Error: object 'x' not found
由于subsample()
函数没有对cond
进行引用,所以导致subset2()
无法正确评估x
。这种嵌套传递“表达式”需要“引用-解引用”式的中间步骤。
<- function(df, cond, n = nrow(df)) {
subsample <- enquo(cond)
cond <- subset2(df, !!cond)
df resample(df, n)
}
subsample(df, x == 1)
#> x y
#> 1 1 1
#> 1.1 1 1
#> 2 1 2
Handling ambiguity
当既有指向数据框的参数也有指向环境的参数时,会导致引用歧义,产生不符合预期的结果。
假设现在有一个根据提供的参数值来过滤数据框的函数。
<- function(df, val) {
threshold_x subset2(df, x >= val)
}
- 当
x
在环境中存在,但不在数据框中时:
<- 10
x <- data.frame(y = 1:3)
no_x threshold_x(no_x, 2)
#> y
#> 1 1
#> 2 2
#> 3 3
- 当数据框中有
val
列时:
<- data.frame(x = 1:3, val = 9:11)
has_val threshold_x(has_val, 2)
#> [1] x val
#> <0 rows> (or 0-length row.names)
特殊情况会产生不符合预期的结果,所以我们需要声明参数来源。
<- function(df, val) {
threshold_x subset2(df, .data$x >= .env$val)
}
<- 10
x threshold_x(no_x, 2)
#> Error in `.data$x`:
#> ! Column `x` not found in `.data`.
threshold_x(has_val, 2)
#> x val
#> 2 2 10
#> 3 3 11
通常使用.env
声明的参数也可以使用!!
来替代:
<- function(df, val) {
threshold_x subset2(df, .data$x >= !!val)
}
二者的区别在于何时评估参数val
:如果使用!!
,val
会被enquo()
评估,如果使用.env
,val
会被eval_tidy()
评估。这种差别的影响微乎其微。
Quoting and ambiguity
本小节的内容,没有搞懂作者的意图。
将上面的threshold_x()
函数的筛选列由固定的x
改为参数var
提供,可以使用.data[[var]]
来访问列:
<- function(df, var, val) {
threshold_var <- as_string(ensym(var))
var subset2(df, .data[[var]] >= !!val)
}
<- data.frame(x = 1:10)
df threshold_var(df, x, 8)
#> x
#> 8 8
#> 9 9
#> 10 10
也可以使用enquo()
和!!
来处理列:
<- function(df, expr, val) {
threshold_expr <- enquo(expr)
expr subset2(df, !!expr >= !!val)
}
threshold_expr(df, x, 8)
#> x
#> 8 8
#> 9 9
#> 10 10
Base evaluation
本节介绍base R中替代tidy evaluation的两种常用函数:
substitute()
和在调用环境中评估(base::subset()
使用的函数)。match.call()
控制调用和在调用环境中评估(stats::lm()
使用的函数)。
substitute()
base R中最常见的非标准性评估(NSE)模式是substitute()
+ eval()
。下面是使用这种模式编写的subset()
。二者的主要区别是评估的环境不同,前者在调用环境中评估,后者在“表达式”定义时的环境中评估。
<- function(data, rows) {
subset_base <- substitute(rows)
rows <- eval(rows, data, caller_env())
rows_val stopifnot(is.logical(rows_val))
= FALSE]
data[rows_val, , drop
}
<- function(data, rows) {
subset_tidy <- enquo(rows)
rows <- eval_tidy(rows, data, env = caller_env())
rows_val stopifnot(is.logical(rows_val))
= FALSE]
data[rows_val, , drop }
Programming with subset()
subset()
的文档中由这样的警告:
This is a convenience function intended for use interactively. For programming it is better to use the standard subsetting functions like [
, and in particular the non-standard evaluation of argument subset can have unanticipated consequences.
它存在三个主要问题:
base::subset()
总是在调用环境中评估rows
,这可能会导致评估错误。<- function(df, ...) { f1 <- 3 xval subset_base(df, ...) # subset_tidy(df, ...) } <- data.frame(x = 1:3, y = 3:1) my_df <- 1 xval f1(my_df, x == xval) #> x y #> 3 3 1
这也意味着
subset_base()
类型的函数不能与map()
或lapply()
一起使用。local({ <- 2 zzz <- list(data.frame(x = 1:3), data.frame(x = 4:6)) dfs lapply(dfs, subset_base, x == zzz) })#> Error in eval(rows, data, caller_env()): object 'zzz' not found
从另一个函数调用
subset()
需要注意:你必须使用substitute()
来捕获对substitute()
完整表达式的调用,然后进行求值。这段代码很难理解,因为substitute()
没有使用语法标记来取消引用。<- function(df1, expr) { f2 <- substitute(subset_base(df1, expr)) call expr_print(call) eval(call, caller_env()) } <- data.frame(x = 1:3, y = 3:1) my_df f2(my_df, x == 1) #> subset_base(my_df, x == 1) #> x y #> 1 1 3
eval()
不提供任何“声明”,所以无法准确区分“表达式”来源。据我所知,除非手动检查df
中是否存在z
变量,否则无法确保以下函数的安全。<- function(df) { f3 <- substitute(subset_base(df, z > 0)) call expr_print(call) eval(call, caller_env()) } <- data.frame(x = 1:3, y = 3:1) my_df <- -1 z f3(my_df) #> subset_base(my_df, z > 0) #> [1] x y #> <0 rows> (or 0-length row.names)
What about [
?
既然tidy-eval很复杂,为什么不直接使用[
?首先,[
只能交互使用,不能应用在函数中,会不具有通用性。其次,相较于[
,subset()
有两个有点:
它默认设置了
drop = FALSE
,保证返回的始终是数据框。它丢掉了条件是
NA
的行。
这意味着,subset(df, x == y)
不等于df[x == y, ]
,而是等价于df[x == y & !is.na(x == y), , drop = FALSE]
。而且,类似subset()
的函数,如dplyr::filter()
甚至可以将R语言的过滤规则转化为SQL语言,使得它在编程方面应用广泛。
match.call()
base R中另外一种NSE模式是使用match.call()
。与substitute()
类似,都会捕获”表达式“,但match.call()
会捕获完整的调用”表达式“,并修改然后评估它。”rlang“中没有与其等价的函数。
<- function(x, y, z) {
g match.call()
}g(1, 2, z = 3)
#> g(x = 1, y = 2, z = 3)
match.call()
的一个重要有应用是write.csv()
,write.csv()
捕获调用write.table()
的表达式,然后修改参数,最后评估:
<- function(...) {
write.csv <- match.call(write.table, expand.dots = TRUE)
call
1]] <- quote(write.table)
call[[$sep <- ","
call$dec <- "."
call
eval(call, parent.frame())
}
但是也可以不使用NSE直接实现这种功能:
<- function(...) {
write.csv write.table(..., sep = ",", dec = ".")
}
Wrapping modelling functions
match.call()
的另一个重要应用是lm()
。但这一技术同时也导致打印捕获的”formula“不完整。让我们思考下面这一简单地对lm()
的包装:
<- function(formula, data) {
lm2 lm(formula, data)
}
这个包装函数可以成功运行,但无法准确捕获到”formula“:
lm2(mpg ~ disp, mtcars)
#>
#> Call:
#> lm(formula = formula, data = data)
#>
#> Coefficients:
#> (Intercept) disp
#> 29.59985 -0.04122
为了修复这一错误,我们需要使用准引用技术。
<- function(formula, data, env = caller_env()) {
lm3 <- enexpr(formula)
formula <- enexpr(data)
data
<- expr(lm(!!formula, data = !!data))
lm_call expr_print(lm_call)
eval(lm_call, env)
}lm3(mpg ~ disp, mtcars)
#> lm(mpg ~ disp, data = mtcars)
#>
#> Call:
#> lm(formula = mpg ~ disp, data = mtcars)
#>
#> Coefficients:
#> (Intercept) disp
#> 29.59985 -0.04122
当你想要封装一个base R中的NSE函数时,你需要注意下面三点:
使用
enexpr()
捕获未评估的参数,使用caller_env()
获取调用函数的env。使用
expr()
与!!
组合新的调用“表达式”。要在
caller_env()
中评估新的“表达式”。
Evaluation environment
如果在lm3()
中,对data
进行了某种处理,如resample()
,那么会导致data
的环境由外部的调用环境变为内部的运行环境,最终导致报错。
<- function(formula, data, env = caller_env()) {
resample_lm0 <- enexpr(formula)
formula <- resample(data, n = nrow(data))
resample_data
<- expr(lm(!!formula, data = resample_data))
lm_call expr_print(lm_call)
eval(lm_call, env)
}
<- data.frame(x = 1:10, y = 5 + 3 * (1:10) + round(rnorm(10), 2))
df resample_lm0(y ~ x, data = df)
#> lm(y ~ x, data = resample_data)
#> Error in eval(mf, parent.frame()): object 'resample_data' not found
有两种方法可以避免报错:
直接将
data
解引用进行传递,但这会导致打印出的data
很奇怪:<- function(formula, data, env = caller_env()) { resample_lm1 <- enexpr(formula) formula <- resample(data, n = nrow(data)) resample_data <- expr(lm(!!formula, data = !!resample_data)) lm_call expr_print(lm_call) eval(lm_call, env) }resample_lm1(y ~ x, data = df)$call #> lm(y ~ x, data = <df[,2]>) #> lm(formula = y ~ x, data = list(x = c(10L, 7L, 6L, 8L, 7L, 6L, #> 9L, 5L, 7L, 6L), y = c(34.45, 24.36, 22.83, 28.5, 24.36, 22.83, #> 32.83, 19.01, 24.36, 22.83)))
将修改后的
data
重新添加到caller_env()
中:<- function(formula, data, env = caller_env()) { resample_lm2 <- enexpr(formula) formula <- resample(data, n = nrow(data)) resample_data <- env(env, resample_data = resample_data) lm_env <- expr(lm(!!formula, data = resample_data)) lm_call expr_print(lm_call) eval(lm_call, lm_env) }resample_lm2(y ~ x, data = df) #> lm(y ~ x, data = resample_data) #> #> Call: #> lm(formula = y ~ x, data = resample_data) #> #> Coefficients: #> (Intercept) x #> 4.961 2.915